/*******************************************************************************
 * This file is part of blueprint4j www.blueprint4j.org.
 * 
 *     blueprint4j is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     blueprint4j is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with blueprint4j. If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package blueprint4j.db;

/**
 * DBConnection.java
 * This class holds connections, if connection is broken then it recreates it
 */

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Hashtable;
import java.util.Vector;

import blueprint4j.utils.Log;
import blueprint4j.utils.Utils;
import blueprint4j.utils.VectorString;

public class DBConnection implements java.io.Serializable{
	private static final long serialVersionUID = 1L;

	// keeps the connection
	private Connection dbcon = null;
	//private DBReference dbcon = null;
	// keeps the connection details
	private String classForName = null;
	private String databasename = null;
	private String url = null;
	private String path = null;
	private String host = null;
	private String username = null;
	private String password = null;

	private static Hashtable<String,DataBaseUtils> databaseutils = new Hashtable<String,DataBaseUtils>();
	// private static Hashtable databases = new Hashtable();
	private static Hashtable<String,VectorString> tables = new Hashtable<String,VectorString>();
	private static Hashtable<String,Index.VectorIndex> indexes = new Hashtable<String,Index.VectorIndex>();
	private static Hashtable<String,Object> updated_tables = new Hashtable<String,Object>();

	private boolean auto_commit = false;
	private boolean allow_close = true;

	private Vector<NewEvent> new_events=new Vector<NewEvent>();
	private Vector<DeleteEvent> delete_events=new Vector<DeleteEvent>();
	private Vector<FieldChangeEvent> change_events=new Vector<FieldChangeEvent>();

	private String meta_db_version = null;
	private int meta_db_minor_version = 0;
	private int meta_db_major_version = 0;

	private String stacktrace= null,closestacktrace = null;

	// IF TEH DBCON SHOULD BE RETURNED TO A CONNECTION POOL WHEN CLOSED
	private ConnectionPool connectionPool = null;

	public DBConnection(ConnectionPool connectionPool,String p_classForName, String p_dbname,String p_host,String p_url, String p_path,String p_username, String p_password, boolean p_allow_close)
			throws DataException, SQLException {
		this(p_classForName,p_dbname,p_host,p_url,p_path,p_username,p_password);
		this.allow_close = p_allow_close;
		this.connectionPool = connectionPool;
	}

	/**
	 * public DBConnection(String p_classForName,String p_url,String p_username,String p_password)
	 * throws DataException,SQLException
	 *
	 * Constructs a class using its url, username and password parameters
	 */
	public DBConnection(String p_classForName, String p_dbname,String p_host,String p_url, String p_path,String p_username, String p_password)
			throws DataException, SQLException {
		if (p_classForName == null || p_url == null)
			throw new DataException("Can not create a connection uring an empty url/class loader");
		classForName = p_classForName;
		url = p_url;
		username = p_username;
		password = p_password;
		databasename = p_dbname;
		path = p_path;
		host = p_host;
		//databasename = databasename.substring(databasename.indexOf("/")+1);
		if (databasename.indexOf("?")!=-1) {
			databasename = databasename.substring(0,databasename.indexOf("?"));
		}
		setDatabaseUtils(url);
		try {
			Class.forName(classForName);
		} catch (ClassNotFoundException cnfe) {
			cnfe.printStackTrace();
			throw new DataException("Error creating connection: " + cnfe.getMessage());
		}
		dbcon = java.sql.DriverManager.getConnection(url, username, password);
		getDBConnection().setAutoCommit(false);
		stacktrace = Utils.getStackTraceStr();
	}

	public DBConnection getNewInstance()
			throws SQLException {
		return new DBConnection(connectionPool,classForName,databasename,host,url,path,username,password,allow_close);
	}

	public String getDatabaseVersion()
			throws SQLException {
		if (meta_db_version == null) {
			meta_db_version = getDBConnection().getMetaData().getDatabaseProductVersion();
		}
		return meta_db_version;
	}

	public int getDatabaseMinorVersion()
			throws SQLException {
		if (meta_db_minor_version == 0) {
			meta_db_minor_version = getDBConnection().getMetaData().getDatabaseMinorVersion();
		}
		return meta_db_minor_version;
	}

	public int getDatabaseMajorVersion()
			throws SQLException {
		if (meta_db_major_version == 0) {
			meta_db_major_version = getDBConnection().getMetaData().getDatabaseMajorVersion();
		}
		return meta_db_major_version;
	}

	public static DataBaseUtils getDataBaseUtils(String url)
			throws DataException {
		if (url.indexOf("mysql")!=-1) {
			return new DataBaseUtilsMySQL();
		} else if (url.indexOf("informix")!=-1) {
			return new DataBaseUtilsInformix();
		} else if (url.indexOf("sqlserver")!=-1) {
			return new DataBaseUtilsMSSQL();
		} else if (url.indexOf("firebirdsql")!=-1) {
			return new DataBaseUtilsFireBird();
		} else if (url.indexOf("hsql")!=-1) {
			return new DataBaseUtilsHSQL();
		} else if (url.indexOf("derby")!=-1) {
			return new DataBaseUtilsDerby();
		}else {
			throw new DataException("TYPE JDBC URL["+url+"] COULD NOT BE IDENTIFIED OR IS NOT SUPPORTED");
		}
	}

	public boolean allowClose() {
		return allow_close;
	}

	/**
	 * Determines the right type of database utils to use based on the url
	 * jdbc:mysql is mysql and so on
	 */
	private void setDatabaseUtils(String url)
			throws SQLException {
		if (!databaseutils.containsKey(url)) {
			databaseutils.put(url,getDataBaseUtils(url));
		}
	}

	public String getPath() {
		return path;
	}

	public String getHost() {
		return host;
	}

	public DataBaseUtils getDataBaseUtils() {
		return (DataBaseUtils) databaseutils.get(url);
	}

	public VectorString getTables()
			throws SQLException {
		if (!tables.containsKey(url)) {
			VectorString t = new VectorString();
			for (ResultSet rs = getDBConnection().getMetaData().getTables(null,null,null,new String[]{"TABLE"});
			rs.next();) {
				t.add(rs.getString(3).toLowerCase());
			}
			tables.put(url,t);
		}
		return (VectorString) tables.get(url);
	}

	public Index.VectorIndex getIndexes()
			throws SQLException {
		if (!indexes.containsKey(url)) {
			Index.VectorIndex index = new Index.VectorIndex();
			VectorString tables = getTables();
			for (int i = 0;i < tables.size();i++) {
				index.add(Index.VectorIndex.getIndexForTable(this,tables.get(i)));
			}
			indexes.put(url,index);
			return index;
		} else {
			return (Index.VectorIndex) indexes.get(url);
		}
	}

	public void addTable(String tablename)
			throws SQLException {
		if (!getTables().containsIgnoreCase(tablename)) {
			getTables().add(tablename);
		}
	}

	public boolean hasTableBeenUpdated(String tablename) {
		synchronized (updated_tables) {
			if (!updated_tables.containsKey(url)) {
				updated_tables.put(url,new Hashtable());
			}
			if (!((Hashtable)updated_tables.get(url)).containsKey(tablename)) {
				((Hashtable)updated_tables.get(url)).put(tablename,new Object());
				return false;
			}
			return true;
		}
	}

	public String getUrl() {
		return url;
	}

	public String getPassword() {
		return password;
	}

	public String getUsername() {
		return username;
	}

	/*
		  public Connection getConnection()
		  throws DataException {
					 return dbcon;
		  }*/

	public String getDatabaseName() {
		return databasename;
	}

	public boolean isClosed()
			throws SQLException {
		return getDBConnection().isClosed();
	}

	public int execute(String sql)
			throws SQLException {
		System.out.println(sql);
		Statement statement = getDBConnection().createStatement();
		try {
			Log.sql.out(sql);
			return statement.executeUpdate(sql);
		} finally {
			if (statement != null) {
				statement.close();
			}
		}
	}

	public Connection getDBConnection() {
		return (Connection) dbcon;
	}

	public DatabaseMetaData getMetaData() throws SQLException {
		return getDBConnection().getMetaData();
	}

	public Statement createStatement()
			throws SQLException {
		return getDBConnection().createStatement();
	}

	public PreparedStatement prepareStatement(String sql) throws SQLException {
		return getDBConnection().prepareStatement(sql);
	}



	public void setAutoCommit(boolean flag)
			throws SQLException {
		auto_commit = flag;
		getDBConnection().setAutoCommit(auto_commit);
	}

	public void commit()
			throws SQLException {
		try {
			if (!auto_commit) {
				while (new_events.size()>0){
					((NewEvent)new_events.remove(0)).execute();
				}
				while (change_events.size()>0){
					((FieldChangeEvent)change_events.remove(0)).execute();
				}
				while (delete_events.size()>0){
					((DeleteEvent)delete_events.remove(0)).execute();
				}
				getDBConnection().commit();
			}
		} catch (Throwable th) {
			Log.debug.out("DBConnection Exception. Create Stack Trace",stacktrace);
			if (closestacktrace != null) {
				Log.debug.out("DBConnection Exception. Close Stack Trace",closestacktrace);
			}

			throw new DataException("DBException Occurred. Current Stack Trace " + th.getMessage(),th);
		}
	}

	public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
		return getDBConnection().createStatement(resultSetType,resultSetConcurrency);
	}

	public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
		return getDBConnection().createStatement(resultSetType,resultSetConcurrency,resultSetHoldability);
	}

	public void rollback()
			throws SQLException {
		new_events.clear();
		change_events.clear();
		delete_events.clear();
		if (!auto_commit) {
			getDBConnection().rollback();
		} else{
			throw new SQLException("Can not rollback on an autocommit connection","Created Stacktrace:"+stacktrace);
		}
	}

	public void close()
			throws SQLException {
		if (closestacktrace == null) {
			closestacktrace = Utils.getStackTraceStr();
		}
		new_events.clear();
		change_events.clear();
		delete_events.clear();
		if (allow_close) {
			if (connectionPool != null) {
				if (!auto_commit) {
					getDBConnection().rollback();
				}
				setAutoCommit(false);
				connectionPool.connectionAvailable(this);
			} else {
				getDBConnection().close();
			}
		} else {
			Log.debug.out("NO CLOSE OPERATION ALLOWD ON THIS CONNECTION: Created Stacktrace: "+stacktrace,new Exception());
		}
	}

	public void bulkLoadFile(DBConnection dbcon,String tablename,java.io.File file,String delimiter)
			throws SQLException {
		Log.trace.out("STARTED BULKLOADING","FILE ["+file.getName()+"] INTO TABLE ["+tablename+"]");
		getDataBaseUtils().bulkLoadFile(this,tablename,file,delimiter);
		Log.trace.out("STARTED BULKLOADING","COMPLETED");
	}

	void addNewEvent(NewEntityListener listener,Entity entity)
			throws SQLException{
		if (auto_commit){
			listener.newEntity(DBConnection.this,entity);
		}else{
			new_events.add(new NewEvent(listener,entity));
		}
	}

	void addDeleteEvent(DeleteEntityListener listener,Entity entity)
			throws SQLException{
		if (auto_commit){
			listener.deleteEntity(DBConnection.this,entity);
		}else{
			delete_events.add(new DeleteEvent(listener,entity));
		}
	}

	void addChangeEvent(ChangeFieldListener listener,Field field)
			throws SQLException{
		if (auto_commit){
			listener.fieldChanged(DBConnection.this,field);
		}else{
			change_events.add(new FieldChangeEvent(listener,field));
		}
	}

	private class NewEvent {
		private NewEntityListener listener;
		private Entity entity;

		private NewEvent(NewEntityListener listener,Entity entity){
			this.listener=listener;
			this.entity=entity;
		}

		private void execute()
				throws SQLException{
			listener.newEntity(DBConnection.this,entity);
		}
	}

	private class DeleteEvent {
		private DeleteEntityListener listener;
		private Entity entity;

		private DeleteEvent(DeleteEntityListener listener,Entity entity){
			this.listener=listener;
			this.entity=entity;
		}

		private void execute()
				throws SQLException{
			listener.deleteEntity(DBConnection.this,entity);
		}
	}

	private class FieldChangeEvent {
		private ChangeFieldListener listener;
		private Field field;

		private FieldChangeEvent(ChangeFieldListener listener,Field field){
			this.listener=listener;
			this.field=field;
		}

		private void execute()
				throws SQLException{
			listener.fieldChanged(DBConnection.this,field);
		}
	}

	public void restoreDB(java.io.File dbfile)
			throws Throwable {
		getDataBaseUtils().restoreDB(this,dbfile);
	}

	public void backupDB(java.io.File dbfile)
			throws Throwable {
		getDataBaseUtils().backupDB(this,dbfile);
	}

}
